/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.solr.search.function;

import org.apache.lucene.index.IndexReader;
import org.apache.lucene.search.*;
import org.apache.solr.search.SolrIndexReader;

import java.io.IOException;
import java.util.Set;
import java.util.Map;

/**
 * Returns a score for each document based on a ValueSource, often some function
 * of the value of a field.
 * 
 * <b>Note: This API is experimental and may change in non backward-compatible
 * ways in the future</b>
 * 
 * @version $Id: FunctionQuery.java 1065312 2011-01-30 16:08:25Z rmuir $
 */
public class FunctionQuery extends Query {
	ValueSource func;

	/**
	 * @param func
	 *            defines the function to be used for scoring
	 */
	public FunctionQuery(ValueSource func) {
		this.func = func;
	}

	/** @return The associated ValueSource */
	public ValueSource getValueSource() {
		return func;
	}

	@Override
	public Query rewrite(IndexReader reader) throws IOException {
		return this;
	}

	@Override
	public void extractTerms(Set terms) {
	}

	protected class FunctionWeight extends Weight {
		protected Searcher searcher;
		protected float queryNorm;
		protected float queryWeight;
		protected Map context;

		public FunctionWeight(Searcher searcher) throws IOException {
			this.searcher = searcher;
			this.context = func.newContext();
			func.createWeight(context, searcher);
		}

		@Override
		public Query getQuery() {
			return FunctionQuery.this;
		}

		@Override
		public float getValue() {
			return queryWeight;
		}

		@Override
		public float sumOfSquaredWeights() throws IOException {
			queryWeight = getBoost();
			return queryWeight * queryWeight;
		}

		@Override
		public void normalize(float norm) {
			this.queryNorm = norm;
			queryWeight *= this.queryNorm;
		}

		@Override
		public Scorer scorer(IndexReader reader, boolean scoreDocsInOrder,
				boolean topScorer) throws IOException {
			return new AllScorer(getSimilarity(searcher), reader, this);
		}

		@Override
		public Explanation explain(IndexReader reader, int doc)
				throws IOException {
			SolrIndexReader topReader = (SolrIndexReader) reader;
			SolrIndexReader[] subReaders = topReader.getLeafReaders();
			int[] offsets = topReader.getLeafOffsets();
			int readerPos = SolrIndexReader.readerIndex(doc, offsets);
			int readerBase = offsets[readerPos];
			return ((AllScorer) scorer(subReaders[readerPos], true, true))
					.explain(doc - readerBase);
		}
	}

	protected class AllScorer extends Scorer {
		final IndexReader reader;
		final FunctionWeight weight;
		final int maxDoc;
		final float qWeight;
		int doc = -1;
		final DocValues vals;
		final boolean hasDeletions;

		public AllScorer(Similarity similarity, IndexReader reader,
				FunctionWeight w) throws IOException {
			super(similarity, w);
			this.weight = w;
			this.qWeight = w.getValue();
			this.reader = reader;
			this.maxDoc = reader.maxDoc();
			this.hasDeletions = reader.hasDeletions();
			vals = func.getValues(weight.context, reader);
		}

		@Override
		public int docID() {
			return doc;
		}

		@Override
		// instead of matching all docs, we could also embed a query.
		// the score could either ignore the subscore, or boost it.
		// Containment: floatline(foo:myTerm, "myFloatField", 1.0, 0.0f)
		// Boost: foo:myTerm^floatline("myFloatField",1.0,0.0f)
		public int nextDoc() throws IOException {
			for (;;) {
				++doc;
				if (doc >= maxDoc) {
					return doc = NO_MORE_DOCS;
				}
				if (hasDeletions && reader.isDeleted(doc))
					continue;
				return doc;
			}
		}

		@Override
		public int advance(int target) throws IOException {
			// this will work even if target==NO_MORE_DOCS
			doc = target - 1;
			return nextDoc();
		}

		// instead of matching all docs, we could also embed a query.
		// the score could either ignore the subscore, or boost it.
		// Containment: floatline(foo:myTerm, "myFloatField", 1.0, 0.0f)
		// Boost: foo:myTerm^floatline("myFloatField",1.0,0.0f)
		public boolean next() throws IOException {
			for (;;) {
				++doc;
				if (doc >= maxDoc) {
					return false;
				}
				if (hasDeletions && reader.isDeleted(doc))
					continue;
				// todo: maybe allow score() to throw a specific exception
				// and continue on to the next document if it is thrown...
				// that may be useful, but exceptions aren't really good
				// for flow control.
				return true;
			}
		}

		public int doc() {
			return doc;
		}

		@Override
		public float score() throws IOException {
			float score = qWeight * vals.floatVal(doc);

			// Current Lucene priority queues can't handle NaN and -Infinity, so
			// map to -Float.MAX_VALUE. This conditional handles both -infinity
			// and NaN since comparisons with NaN are always false.
			return score > Float.NEGATIVE_INFINITY ? score : -Float.MAX_VALUE;
		}

		public boolean skipTo(int target) throws IOException {
			doc = target - 1;
			return next();
		}

		public Explanation explain(int doc) throws IOException {
			float sc = qWeight * vals.floatVal(doc);

			Explanation result = new ComplexExplanation(true, sc,
					"FunctionQuery(" + func + "), product of:");

			result.addDetail(vals.explain(doc));
			result.addDetail(new Explanation(getBoost(), "boost"));
			result.addDetail(new Explanation(weight.queryNorm, "queryNorm"));
			return result;
		}
	}

	@Override
	public Weight createWeight(Searcher searcher) throws IOException {
		return new FunctionQuery.FunctionWeight(searcher);
	}

	/** Prints a user-readable version of this query. */
	@Override
	public String toString(String field) {
		float boost = getBoost();
		return (boost != 1.0 ? "(" : "") + func.toString()
				+ (boost == 1.0 ? "" : ")^" + boost);
	}

	/** Returns true if <code>o</code> is equal to this. */
	@Override
	public boolean equals(Object o) {
		if (FunctionQuery.class != o.getClass())
			return false;
		FunctionQuery other = (FunctionQuery) o;
		return this.getBoost() == other.getBoost()
				&& this.func.equals(other.func);
	}

	/** Returns a hash code value for this object. */
	@Override
	public int hashCode() {
		return func.hashCode() * 31 + Float.floatToIntBits(getBoost());
	}

}
